// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using SnapObjects.Commons;

namespace SnapObjects.Data
{
    /// <summary>
    ///     Represents a connection to the database.
    /// </summary>
    public abstract class AdoDbConnection : IAdoDbConnection
    {
        private readonly string _connectionString;
        private readonly LazyReference<DbConnection> _connection;
        private readonly bool _connectionOwned;
        private int _openedCount;
        private bool _openedInternally;
        private bool _disposed;

        protected AdoDbConnection(string connectionString)
        {
            if (!String.IsNullOrWhiteSpace(connectionString))
            {
                _connectionString = connectionString;
                _connection = new LazyReference<DbConnection>(this.CreateDbConnection);
                _connectionOwned = true;
            }
            else
            {
                throw new InvalidOperationException("Invalid connection string.");
            }
        }

        protected AdoDbConnection(DbConnection dbConnection)
        {
            if (dbConnection != null)
            {
                _connection = new LazyReference<DbConnection>(() => dbConnection);
                _connectionOwned = false;
            }
            else
            {
                throw new InvalidOperationException("DbConnection is null.");
            }
        }

        protected abstract DbConnection CreateDbConnection();

        /// <summary>
        ///     Gets the GUID of this IAdoDbConnection object.
        /// </summary>
        public virtual Guid ConnectionId { get; } = Guid.NewGuid();

        /// <summary>
        ///     Gets the connection string used to open the database.
        /// </summary>
        public virtual string ConnectionString => _connectionString ?? _connection.Value.ConnectionString;

        /// <summary>
        ///     Gets the internal DbConnection object.
        /// </summary>
        public virtual DbConnection DbConnection => _connection.Value;

        /// <summary>
        ///     Gets an IAdoDbTransaction object which represents the current database transaction.
        /// </summary>
        public virtual IAdoDbTransaction CurrentTransaction { get; protected set; }

        /// <summary>
        ///     Starts a database transaction without specifying the isolation level (IsolationLevel.Unspecified).
        /// </summary>
        /// <returns>Returns an object representing the new transaction if succeeds.</returns>
        public virtual IAdoDbTransaction BeginTransaction()
        {
            return this.BeginTransaction(IsolationLevel.Unspecified);
        }

        /// <summary>
        ///     Starts a database transaction with the specified isolation level.
        /// </summary>
        /// <param name="isolationLevel">The isolation level for the transaction.</param>
        /// <returns>Returns an object representing the new transaction if succeeds.</returns>
        public virtual IAdoDbTransaction BeginTransaction(IsolationLevel isolationLevel)
        {
            if (this.CurrentTransaction != null)
            {
                throw new InvalidOperationException("Transaction already started.");
            }

            this.Open();

            var dbTransaction = this.DbConnection.BeginTransaction(isolationLevel);

            this.CurrentTransaction = new AdoDbTransaction(this, dbTransaction, true);

            return this.CurrentTransaction;
        }

        public virtual Task<IAdoDbTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
        {
            return this.BeginTransactionAsync(IsolationLevel.Unspecified, cancellationToken);
        }

        public async virtual Task<IAdoDbTransaction> BeginTransactionAsync(
            IsolationLevel isolationLevel, CancellationToken cancellationToken = default)
        {
            if (this.CurrentTransaction != null)
            {
                throw new InvalidOperationException("Transaction already started.");
            }

            await this.OpenAsync(cancellationToken);

            var dbTransaction = this.DbConnection.BeginTransaction(isolationLevel);

            this.CurrentTransaction = new AdoDbTransaction(this, dbTransaction, true);

            return this.CurrentTransaction;
        }

        /// <summary>
        ///     Uses a specified database transaction when no transaction is already started.
        /// </summary>
        /// <param name="transaction">The database transaction you want to use in the current database connection.</param>
        /// <returns>Returns an IAdoDbTransaction object that represents the new database transaction if succeeds.</returns>
        public virtual IAdoDbTransaction UseTransaction(DbTransaction transaction)
        {
            if (transaction == null)
            {
                if (this.CurrentTransaction != null)
                {
                    this.CurrentTransaction = null;
                }
            }
            else
            {
                if (this.CurrentTransaction != null)
                {
                    throw new InvalidOperationException("Transaction already started.");
                }

                this.CurrentTransaction = new AdoDbTransaction(this, transaction, false);
            }

            return this.CurrentTransaction;
        }

        /// <summary>
        ///     Opens the database connection using the connection string.
        /// </summary>
        /// <returns>
        ///     Returns false if the current state of the connection is ConnectionState.Open; otherwise, 
        ///     returns true if a database connection is opened successfully.
        /// </returns>
        public virtual bool Open()
        {
            if (_connection.Value.State == ConnectionState.Broken)
            {
                _connection.Value.Close();
            }

            var wasOpened = false;

            if (_connection.Value.State != ConnectionState.Open)
            {
                _connection.Value.Open();

                wasOpened = true;

                if (_openedCount == 0)
                {
                    _openedInternally = true;
                }
            }

            _openedCount++;

            return wasOpened;
        }

        public virtual Task<bool> OpenAsync()
        {
            return this.OpenAsync(default);
        }

        public async virtual Task<bool> OpenAsync(CancellationToken cancellationToken)
        {
            if (_connection.Value.State == ConnectionState.Broken)
            {
                await _connection.Value.CloseAsync();
            }

            var wasOpened = false;

            if (_connection.Value.State != ConnectionState.Open)
            {
                await _connection.Value.OpenAsync(cancellationToken);

                wasOpened = true;

                if (_openedCount == 0)
                {
                    _openedInternally = true;
                }
            }

            _openedCount++;

            return wasOpened;
        }

        /// <summary>
        ///     Closes the connection to the database.
        /// </summary>
        /// <returns>
        ///     Returns false if the current state of the connection is ConnectionState.Closed or this connection 
        ///     was not opened in the current IAdoDbConnection object; otherwise, returns true if a database 
        ///     connection is closed successfully.
        /// </returns>
        public virtual bool Close()
        {
            var wasClosed = false;

            if (this.ShouldClose())
            {
                if (_connection.Value.State != ConnectionState.Closed)
                {
                    _connection.Value.Close();

                    wasClosed = true;
                }

                _openedInternally = false;
            }

            return wasClosed;
        }

        public virtual async Task<bool> CloseAsync()
        {
            var wasClosed = false;

            if (this.ShouldClose())
            {
                if (_connection.Value.State != ConnectionState.Closed)
                {
                    await _connection.Value.CloseAsync();

                    wasClosed = true;
                }

                _openedInternally = false;
            }

            return wasClosed;
        }

        public virtual void Dispose()
        {
            if (!_disposed)
            {
                _disposed = true;

                this.CurrentTransaction?.Dispose();

                if (_connectionOwned && _connection.HasValue)
                {
                    _connection.Value.Dispose();
                    _connection.Reset(this.CreateDbConnection);
                    _openedCount = 0;
                    _openedInternally = false;
                }
            }
        }

        private bool ShouldClose()
        {
            return (_openedCount == 0 || (_openedCount > 0 && --_openedCount == 0)) && _openedInternally;
        }
    }
}
